#include "common.h"
#include "memory/memory.h"
#include "device/mmio.h"
#include "memory/cache.h"
#include "macro.h"
#include "misc.h"
#include "memory/l2cache.h"
#include <stdlib.h>
#include <inttypes.h>

#define CACHE_NUM_SETS (CACHE_SIZE / (CACHE_BLOCK_SIZE * CACHE_ASSOC))

typedef struct CacheLine {
    bool valid;
    uint32_t tag;
    uint8_t data[CACHE_BLOCK_SIZE];
} CacheLine;

static CacheLine cache_sets[CACHE_NUM_SETS][CACHE_ASSOC];
static uint64_t cache_cycles = 0;
static uint64_t cache_hits = 0;
static uint64_t cache_misses = 0;

static inline uint32_t get_block_offset(hwaddr_t addr) { return addr & (CACHE_BLOCK_SIZE - 1); }
static inline uint32_t get_index(hwaddr_t addr) { return (addr / CACHE_BLOCK_SIZE) % CACHE_NUM_SETS; }
static inline uint32_t get_tag(hwaddr_t addr) { return (uint32_t)(addr / CACHE_BLOCK_SIZE) / CACHE_NUM_SETS; }

void cache_init(void) {
    int i, j;
    for (i = 0; i < CACHE_NUM_SETS; i++) {
        for (j = 0; j < CACHE_ASSOC; j++) {
            cache_sets[i][j].valid = false;
            cache_sets[i][j].tag = 0;
        }
    }
}

void cache_reset_stats(void) {
    cache_cycles = 0;
    cache_hits = 0;
    cache_misses = 0;
}

static inline uint32_t choose_victim(uint32_t set_idx) {
    int j;
    for (j = 0; j < CACHE_ASSOC; j++) {
        if (!cache_sets[set_idx][j].valid) return j;
    }
    return rand() % CACHE_ASSOC;
}

static inline int try_mmio(hwaddr_t addr) {
    return is_mmio(addr);
}

uint32_t cache_hwaddr_read(hwaddr_t addr, size_t len) {
    Assert(len == 1 || len == 2 || len == 4, "len must be 1/2/4");

    int map_NO = try_mmio(addr);
    if (map_NO != -1) {
        return mmio_read(addr, len, map_NO);
    }

    uint32_t offset = get_block_offset(addr);
    uint32_t set_idx = get_index(addr);
    uint32_t tag = get_tag(addr);

    if (offset + len > CACHE_BLOCK_SIZE) {
        uint8_t temp[4] = { 0 };
        size_t remaining = len;
        hwaddr_t cur = addr;
        size_t pos = 0;
        while (remaining > 0) {
            uint32_t off = get_block_offset(cur);
            size_t boundary = CACHE_BLOCK_SIZE - off;
            size_t chunk = remaining;
            if (chunk > boundary) chunk = boundary;
            if (chunk >= 2) chunk = 2; else chunk = 1;
            uint32_t v = cache_hwaddr_read(cur, chunk);
            memcpy(temp + pos, &v, chunk);
            cur += chunk;
            pos += chunk;
            remaining -= chunk;
        }
        uint32_t out = 0;
        int i;
        for (i = 0; i < (int)len; i++) {
            out |= ((uint32_t)temp[i]) << (8 * i);
        }
        return out & (~0u >> ((4 - len) << 3));
    }

    int j;
    for (j = 0; j < CACHE_ASSOC; j++) {
        CacheLine* line = &cache_sets[set_idx][j];
        if (line->valid && line->tag == tag) {
            cache_hits++;
            cache_cycles += 2; /* hit cost */
            {
                uint32_t out = 0;
                int i;
                for (i = 0; i < (int)len; i++) {
                    out |= ((uint32_t)line->data[offset + i]) << (8 * i);
                }
                return out;
            }
        }
    }

    uint32_t victim = choose_victim(set_idx);
    hwaddr_t block_base = addr - offset;
    (void)l2_cache_read_block(block_base, cache_sets[set_idx][victim].data);
    cache_sets[set_idx][victim].valid = true;
    cache_sets[set_idx][victim].tag = tag;
    cache_misses++;
    cache_cycles += 200;
    {
        uint8_t* base = cache_sets[set_idx][victim].data;
        uint32_t out = 0;
        int i;
        for (i = 0; i < (int)len; i++) {
            out |= ((uint32_t)base[offset + i]) << (8 * i);
        }
        return out;
    }
}

void cache_hwaddr_write(hwaddr_t addr, size_t len, uint32_t data) {
    Assert(len == 1 || len == 2 || len == 4, "len must be 1/2/4");

    int map_NO = try_mmio(addr);
    if (map_NO != -1) {
        mmio_write(addr, len, data, map_NO);
        return;
    }

    uint32_t offset = get_block_offset(addr);
    if (offset + len > CACHE_BLOCK_SIZE) {
        size_t remaining = len;
        hwaddr_t cur = addr;
        uint32_t shift = 0;
        while (remaining > 0) {
            uint32_t off2 = get_block_offset(cur);
            size_t boundary = CACHE_BLOCK_SIZE - off2;
            size_t chunk = remaining;
            if (chunk > boundary) chunk = boundary;
            if (chunk >= 2) chunk = 2; else chunk = 1;
            uint32_t part = (data >> shift) & (~0u >> ((4 - chunk) << 3));
            cache_hwaddr_write(cur, chunk, part);
            cur += chunk;
            shift += chunk * 8;
            remaining -= chunk;
        }
        return;
    }

    uint32_t set_idx = get_index(addr);
    uint32_t tag = get_tag(addr);

    int j;
    for (j = 0; j < CACHE_ASSOC; j++) {
        CacheLine* line = &cache_sets[set_idx][j];
        if (line->valid && line->tag == tag) {
            {
                uint8_t mask_local[4] = { 0 };
                int i;
                for (i = 0; i < (int)len; i++) mask_local[i] = 1;
                memcpy_with_mask(line->data + offset, &data, len, mask_local);
            }
            cache_hits++;
            cache_cycles += 2;
            break;
        }
    }
    l2_cache_write(addr, len, data);
    if (j == CACHE_ASSOC) {
        cache_misses++;
        cache_cycles += 200;
    }
}

void cache_report_stats(void) {
    uint64_t accesses = cache_hits + cache_misses;
    uint32_t hit_rate_int = accesses ? (uint32_t)(cache_hits * 10000 / accesses) : 0; /* x100 */
    printf("[L1] size=%uB, block=%uB, ways=%u | accesses=%" PRIu64 ", hits=%" PRIu64 ", misses=%" PRIu64 ", hit_rate=%u.%02u%%, cycles=%" PRIu64 "\n",
        (unsigned)CACHE_SIZE, (unsigned)CACHE_BLOCK_SIZE, (unsigned)CACHE_ASSOC,
        accesses, cache_hits, cache_misses, hit_rate_int / 100, hit_rate_int % 100, cache_cycles);
}